﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Xml;
using CSharpExt.Xml;

/*
 * Il nome del CompositeObject potrebbe essere cosi' composto
 * 
 * ""      -> class.Name
 * "name"  -> class.Name.name
 * "@name" -> "name"
 * 
 * fullname  ::= "name/name"
 */

namespace CSharpExt.CObject
{
    public enum When
    {
        Begin, Execute, Done
    }

    /*
     * si deve poter scrivere:
     * 
     *      child.Parent = parent;
     *  
     * e
     * 
     *      parent.Add(child)
     *      
     * ed anche
     * 
     *      child.Parent = null
     *      
     * e
     * 
     *      parent.Remove(child)
     *      parent.Remove(name)
     */

    public class CObject
    {
        #region Private Data

        protected CObject _parent;
        protected Dictionary<String, CObject> _children = new Dictionary<String, CObject>();

        private String _name;
        private String _id = Guid.NewGuid().ToString();

        private bool _configured;

        #endregion

        #region Configuration

        [OParam()]
        protected String _note;

        #endregion

        #region Constructor

        public CObject() : this(null, null)
        {
        }

        public CObject(String name) : this(name, null)
        {
        }

        public CObject(CObject parent) : this(null, parent)
        {
        }

        public CObject(String name, CObject parent)
        {
            setName(name);

            Parent = parent;
        }

        #endregion

        #region Properties

        public String Name
        {
            get { return _name; }
            set { setName(value); }
        }

        public String Id
        {
            get { return _id; }
        }

        public bool IsRoot
        {
            get { return _parent == null; }
        }

        public String Path
        {
            get
            {
                return IsRoot ? String.Format("/{0}", Name) : String.Format("{0}/{1}", _parent.Path, Name);
            }
        }


        public CObject Root
        {
            get { return IsRoot ? this : _parent.Root; }
        }

        public CObject Parent
        {
            get { return _parent; }
            set {
                if (_parent == value)
                    return;

                if (_parent != null)
                    _parent.Remove(this);

                _parent = null;

                if (value != null)
                    value.Add(this);
            }
        }

        public ICollection<CObject> Children
        {
            get { return new List<CObject>(_children.Values).AsReadOnly(); }
        }

        public int Depth
        {
            get { return IsRoot ? 0 : _parent.Depth + 1; }
        }

        #endregion

        #region Operations

        public virtual CObject Configure(String configurePath)
        {
            XmlDocument doc = DocumentResolver.LoadAndResolve(configurePath);

            return Configure(doc.DocumentElement);
        }

        public virtual CObject Configure(XmlElement configuration)
        {
            _configured = true;

            onObjectConfigure(When.Begin);

            configureFields(configuration);

            onObjectConfigure(configuration);

            createInnerObjects(configuration);
            
            configureInnerObjects(configuration);

            setInnerObjectsParent();

            onObjectConfigure(When.Done);

            return this;
        }

        public void Destroy()
        {
            Parent = null;

            onObjectDestroy(When.Begin);

            destroyInnerObjects();

            onObjectDestroy(When.Execute);

            onObjectDestroy(When.Done);
        }


        public CObject this[String name]
        {
            get { return String.IsNullOrEmpty(name) ? this : _children[name]; }
        }


        public CObject Find(String path)
        {
            String[] names = path.Split('/');
            CObject current = path.StartsWith("/") ? rootObject() : this;
            foreach (String name in names)
            {
                if (!_children.ContainsKey(name))
                    return null;

                current = current[name];
            }
            return current;
        }

        public CObject FindById(String id)
        {
            CObject found = this;

            if (Id.Equals(id))
                return found;

            foreach (CObject child in _children.Values)
            {
                if ((found = child.FindById(id)) != null)
                    return found;
            }

            return null;
        }

        public CObject Add(params CObject[] children)
        {
            foreach (CObject child in children)
                addChild(child);
            return this;
        }

        public CObject Remove(String name)
        {
            CObject child;
            if (_children.TryGetValue(name, out child))
                removeChild(child);
            return child;
        }

        public CObject Remove(params CObject[] children)
        {
            foreach (CObject child in children)
                removeChild(child);
            return this;
        }

        #endregion

        #region Parent/Children

        protected virtual void addChild(CObject child)
        {
            if (child == null)
                return;

            if (_children.ContainsKey(child.Name))
                return;

            onAddChild(When.Begin);

            child.setParent(this);

            onAddChild(child);

            onAddChild(When.Done);
        }

        protected virtual void removeChild(CObject child)
        {
            if (child == null)
                return;

            if (!_children.ContainsKey(child.Name))
                return;

            onRemoveChild(When.Begin);

            onRemoveChild(child);
            
            child.setParent(null);

            onRemoveChild(When.Done);
        }

        protected virtual void setParent(CObject parent)
        {
            onParentSet(When.Begin);

            onParentSet(parent);

            onParentSet(When.Done);
        }

        #endregion

        #region Implementation

        internal void setName(String name)
        {
            _name = makeName(GetType().Name, name);
        }

        internal String makeName(String type, String name)
        {
            if (String.IsNullOrEmpty(name))
                return type;
            if (name.StartsWith("@"))
                return name.Substring(1);
            else
                return String.Format("{0}.{1}", GetType().Name, name);
        }

        private CObject rootObject()
        {
            return new CObject("/").Add(Root);
        }

        // -- configuration (simple fields)

        private void configureFields(XmlElement configuration)
        {
            FieldInfo[] fieldList = selectFieldList(true);
            setFieldValues(fieldList, configuration, true);

            fieldList = selectFieldList(false);
            setFieldValues(fieldList, configuration, false);
        }

        // -- notify (inner objects)

        private void createInnerObjects(XmlElement configuration)
        {
            /*XmlNodeList nl = configuration.SelectNodes("Object");
            foreach (XmlElement selectedConfiguration in nl)
            {
                String type = selectedConfiguration.GetAttribute("type");
                String name = selectedConfiguration.GetAttribute("name");

                CObject innerObject = onDeclaredInnerObject(type, name);
                if(innerObject != null)
                    Add(innerObject);
            }*/
            XmlNodeList nl = configuration.SelectNodes("node()");
            foreach (XmlElement selectedConfiguration in nl)
            {
                String type = selectedConfiguration.Name;
                if ("Configuration".Equals(type))
                    continue;

                String name = selectedConfiguration.GetAttribute("name");

                CObject innerObject = onDeclaredInnerObject(type, name);
                if (innerObject != null)
                    Add(innerObject);
            }
        }

        // -- configuration (inner objects)

        private void configureInnerObjects(XmlElement configuration)
        {
            XmlNodeList nl = configuration.SelectNodes("Object");
            foreach (XmlElement innerConfiguration in nl)
            {
                String type = innerConfiguration.GetAttribute("type");
                String name = innerConfiguration.GetAttribute("name");

                String key = makeName(type, name);

                if (_children.ContainsKey(key))
                {
                    _children[key].Configure(innerConfiguration);
                }
                else if (_children.ContainsKey(type))
                {
                    _children[type].Configure(innerConfiguration);
                }
            }
        }

        private void setInnerObjectsParent()
        {
            foreach (String name in _children.Keys)
            {
                this[name].Parent = this;
            }
        }

        private XmlElement __findConfiguration(CObject cobject, XmlElement configuration)
        {
            String ctype = cobject.GetType().Name;

            XmlElement innerConfiguration = null;
            XmlNodeList nl = configuration.SelectNodes("Object");
            int count = 0;
            foreach (XmlElement selectedConfiguration in nl)
            {
                String type = selectedConfiguration.GetAttribute("type");
                String name = selectedConfiguration.GetAttribute("name");

                // la configurazione non si riferisce al tipo di oggetto indicato
                if(!ctype.Equals(type))
                    continue;

                // non e' stato configurato nessun nome
                if(String.IsNullOrEmpty(name))
                {
                    innerConfiguration = selectedConfiguration;
                    ++count;
                    continue;
                }

                // e' stato configurato un nome ed e' lo stesso di questa istanza
                if(name.Equals(_name))
                {
                    innerConfiguration = selectedConfiguration;
                    ++count;
                    continue;
                }
            }

            // sono state trovate 2+ configurazioni relative allo stesso oggetto
            // (una senza ed una con nome? due volte lo stesso nome? due volte senza nome?)
            if(count > 1)
                throw new CObjectException(String.Format("Duplicated configuration for {0}", Name));

            return innerConfiguration;
        }

        // -- destroying (inner objects)

        private void destroyInnerObjects()
        {
            List<String> names = new List<String>(_children.Keys);
            foreach (String name in names)
            {
                CObject child = Remove(name);
                child.Destroy();
            }
        }

        #endregion

        #region Event handling

        // -- Create --------------------------------------------------------

        protected virtual void onObjectConfigure(When when)
        {
            
        }

        protected virtual void onObjectConfigure(XmlElement configuration)
        {
            
        }

        protected virtual CObject onDeclaredInnerObject(String type, String name)
        {
            return null;
        }

        // -- Destroy -------------------------------------------------------

        protected virtual void onObjectDestroy(When when)
        {
            
        }

        // -- Parent/Child --------------------------------------------------

        protected virtual void onAddChild(When when)
        {
            
        }

        protected virtual void onAddChild(CObject child)
        {
            _children.Add(child.Name, child);
        }

        protected virtual void onRemoveChild(When when)
        {
            
        }

        protected virtual void onRemoveChild(CObject child)
        {
            _children.Remove(child.Name);
        }

        protected virtual void onParentSet(When when)
        {

        }

        protected virtual void onParentSet(CObject parent)
        {
            _parent = parent;
        }

        #endregion

        #region Configuration
        
        private static Type[] PARSE_SIGNATURE = new Type[] { typeof(string) };
        private static Type[] PARSEFORMATTED_SIGNATURE = new Type[] { typeof(string), typeof(IFormatProvider) };
        private static String[] NO_VALUES = new String[] { };


        private FieldInfo[] selectFieldList(bool mandatory)
        {
            FieldInfo[] fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            FieldInfo[] selectedFields = new FieldInfo[fields.Length];

            int index = 0;
            foreach (FieldInfo fInfo in fields)
            {
                ParamAttribute pattr = (ParamAttribute)Attribute.GetCustomAttribute(fInfo, typeof(ParamAttribute));
                if (pattr != null && pattr.Mandatory == mandatory)
                    selectedFields[index++] = fInfo;
            }

            Array.Resize(ref selectedFields, index);
            return selectedFields;
        }

        private void setFieldValues(FieldInfo[] fields, XmlElement configuration, bool mandatory)
        {
            foreach (FieldInfo fInfo in fields)
            {
                String fieldName = ((fInfo.Name[0] == '_') ? fInfo.Name.Substring(1) : fInfo.Name);
                Type fieldType = fInfo.FieldType;

                String[] svalues = findValues(configuration, fieldName);

                if (mandatory && svalues.Length == 0)
                    throw new MissingParameterException(this, fieldName);

                if (fieldType.IsArray)
                {
                    Type elementType = fInfo.FieldType.GetElementType();
                    Array array = Array.CreateInstance(elementType, svalues.Length);
                    for (int i = 0; i < svalues.Length; ++i )
                    {
                        Object value = parseValue(elementType, svalues[i]);
                        array.SetValue(value, i);
                    }
                    fInfo.SetValue(this, array);
                }
                else if (svalues.Length > 0)
                {
                    Object value = parseValue(fieldType, svalues[0]);
                    fInfo.SetValue(this, value);
                }
            }
        }

        private Object parseValue(Type fieldType, String value)
        {
            if (value == null || fieldType == typeof(String))
            {
                return value;
            }

            // aggiunto il supporto per i tipi enumerativi
            if (fieldType.IsEnum)
            {
                return Enum.Parse(fieldType, value);
            }

            // aggiunto il supporto per interpretare i numeri con la virgola in modo univoco
            // e non in base al locale ('.' e' il punto decimale!)
            if (fieldType.Equals(typeof(float)) || fieldType.Equals(typeof(double)))
            {
                MethodInfo mInfo = fieldType.GetMethod("Parse", PARSEFORMATTED_SIGNATURE);
                if (mInfo != null)
                {
                    return mInfo.Invoke(this, new object[] { value, CultureInfo.InvariantCulture });
                }
            }
            else
            {
                MethodInfo mInfo = fieldType.GetMethod("Parse", PARSE_SIGNATURE);
                if (mInfo != null)
                {
                    return mInfo.Invoke(this, new object[] { value });
                }
            }

            return null;
        }

        // <Parameter name='...' value='...'/>
        // <Parameter name='...'>
        //      <Values value='...'/>
        // </Parameter>
        private String[] findValues(XmlElement configuration, String fieldName)
        {
            String xpath =  String.Format("./Configuration/Parameter[@name='{0}']", fieldName);
            XmlElement selected = (XmlElement) configuration.SelectSingleNode(xpath);
            if (selected == null)
                return NO_VALUES;

            if (selected.HasAttribute("value"))
            {
                return new String[]{ selected.GetAttribute("value") };
            }
            else
            {
                XmlNodeList nl = selected.SelectNodes("./Values");
                if (nl.Count == 0)
                    return NO_VALUES;

                String[] values = new String[nl.Count];
                
                for (int i = 0; i < values.Length; i++)
                {
                    values[i] = ((XmlElement) nl[i]).GetAttribute("value");
                }

                return values;
            }
        }

        #endregion

        #region Overrides

        public override String ToString()
        {
            return Path;
        }

        #endregion
    }

}
